/*
*Copyright 2021-2023 NERCIS
*
*Licensed under the Apache License, Version 2.0 (the "License");
*you may not use this file except in compliance with the License.
*You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*Unless required by applicable law or agreed to in writing, software
*distributed under the License is distributed on an "AS IS" BASIS,
*WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*See the License for the specific language governing permissions and
*limitations under the License.
*/

package cn.ac.nercis.pes.community.component.spreadsheet;

import cn.ac.nercis.pes.community.utils.IconUtils;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TablePositionBase;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import lombok.NonNull;
import org.apache.commons.lang3.StringUtils;
import org.controlsfx.control.spreadsheet.ClipboardCell;
import org.controlsfx.control.spreadsheet.SpreadsheetCell;
import org.controlsfx.control.spreadsheet.SpreadsheetCellType;
import org.controlsfx.control.spreadsheet.SpreadsheetView;

import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 基础Sheet控件
 * @author zpy
 * @version 1.0.0
 */
public abstract class BaseSheetView  extends SpreadsheetView {
    protected final static String REQUIRED_CELL_STYLE = "data-required-cell";
    protected final static String EXCEL_COLUMN_SPLIT = "\t";
    protected final static String EXCEL_ROW_SPLIT = "\n";
    protected final static String EXCEL_CONTENT_ESCAPE = "\"";
    protected final static String EXCEL_CANCEL_ESCAPE = "\"\"";
    protected final static String ESCAPE_CONTENT_REG = "^\"[\\w\\W]*\"$";


    protected final SimpleBooleanProperty editModel = new SimpleBooleanProperty(false);
    protected final SimpleBooleanProperty change = new SimpleBooleanProperty(false);

    public BaseSheetView(){
        this.initKeyEvent();
    }

    public SimpleBooleanProperty changeProperty(){
        return change;
    }

    public void resetChange(){
        this.change.set(false);
    }

    private void initKeyEvent(){
        this.setOnKeyPressed(this::onKeyPressed);
    }

    private void onKeyPressed(KeyEvent keyEvent){
        if(keyEvent.isShortcutDown() && keyEvent.getCode() == KeyCode.C){
            this.copyClipboard();
        }else if(keyEvent.isShortcutDown() && keyEvent.getCode() == KeyCode.V){
            this.pasteClipboard();
        }else if(keyEvent.isShortcutDown() && keyEvent.getCode() == KeyCode.DELETE){
            this.deleteData();
        }
    }

    @Override
    public ContextMenu getSpreadsheetViewContextMenu() {
        return generateContextMenu(true);
    }

    @Override
    public void copyClipboard(){
        var list = new ArrayList<ClipboardCell>();
        var posList = getSelectionModel().getSelectedCells();
        Set<SpreadsheetCell> treatedCells = new HashSet<>();
        for (final TablePosition<?, ?> p : posList) {
            SpreadsheetCell cell = getGrid().getRows().get(getModelRow(p.getRow())).get(getModelColumn(p.getColumn()));
            if (!treatedCells.contains(cell)) {
                treatedCells.add(cell);
                for (int row = 0; row < getRowSpan(cell, p.getRow()); ++row) {
                    for (int col = 0; col < getColumnSpan(cell); ++col) {
                        list.add(new ClipboardCell(p.getRow() + row, p.getColumn() + col, cell));
                    }
                }
            }
        }
        var content = new ClipboardContent();
        content.putString(toExcelContent(list));
        Clipboard.getSystemClipboard().setContent(content);
    }

    @Override
    public void pasteClipboard(){
        var selectedCells = getSelectionModel().getSelectedCells();
        if (!isEditable() || selectedCells.isEmpty()) {
            return;
        }
        final Clipboard clipboard = Clipboard.getSystemClipboard();
        if(StringUtils.isNotBlank(clipboard.getString())){
            pasteExcelContent(clipboard.getString());
        }
    }

    protected String processNewline(String content){
        if(StringUtils.isBlank(content)){
            return content;
        }
        return StringUtils.replace(content,"\\n","\n");
    }

    private void pasteExcelContent(String excelContent){
        int initRow = getSelectionModel().getSelectedCells()
                .stream()
                .map(TablePositionBase::getRow)
                .min(Comparator.comparingInt((number)->number))
                .orElse(0);
        int initCell = getSelectionModel().getSelectedCells()
                .stream()
                .map(TablePositionBase::getColumn)
                .min(Comparator.comparingInt((number)->number))
                .orElse(0);
        var excelRows = parseExcelRows(excelContent);
        editModel.setValue(false);
        for(var index=0; index < excelRows.size(); index++){
            var rowIndex = initRow + index;
            if(rowIndex >= getGrid().getRows().size()){
                break;
            }
            var sheetViewRow = getGrid().getRows().get(rowIndex);
            fullSheetViewCell(excelRows.get(index),initCell,sheetViewRow);
        }
        editModel.setValue(true);
        change.setValue(true);
    }

    private void fullSheetViewCell(String rowContent,Integer initCell,ObservableList<SpreadsheetCell> cells){
        var excelCells = parseExcelCells(rowContent);
        for(var index=0; index < excelCells.size(); index++){
            var cellIndex = initCell + index;
            if(cellIndex >= cells.size()){
                break;
            }
            cells.get(cellIndex).setItem(excelCells.get(index));
        }
    }

    private List<String> parseExcelRows(String content){
        var parseRows = StringUtils.split(content,EXCEL_ROW_SPLIT);
        List<String> excelRows = new ArrayList<>();
        String beforeContent = "";
        for(var index = 0; index<parseRows.length; index++){
            var tempRow = StringUtils.remove(parseRows[index],EXCEL_CANCEL_ESCAPE);
            if(!StringUtils.contains(tempRow,EXCEL_CONTENT_ESCAPE)){
                excelRows.add(parseRows[index]);
            }else{
                if(StringUtils.isBlank(beforeContent)){
                    beforeContent = parseRows[index];
                }else{
                    var escapeCount = StringUtils.countMatches(tempRow,EXCEL_CONTENT_ESCAPE);
                    beforeContent += EXCEL_ROW_SPLIT.concat(parseRows[index]);
                    if(escapeCount<=1){
                        excelRows.add(beforeContent);
                        beforeContent = "";
                    }
                }
            }
        }
        if(StringUtils.isNotBlank(beforeContent)){
            excelRows.add(beforeContent);
        }
        return excelRows.stream()
                .filter(StringUtils::isNotBlank)
                .toList();
    }

    private List<String> parseExcelCells(String row){
        return Stream.of(StringUtils.split(row,EXCEL_COLUMN_SPLIT))
                .map(content->{
                    if(StringUtils.trim(content).matches(ESCAPE_CONTENT_REG)){
                        var tempContent = StringUtils.removeStart(StringUtils.trim(content),EXCEL_CONTENT_ESCAPE);
                        tempContent = StringUtils.replace(tempContent,EXCEL_CANCEL_ESCAPE,EXCEL_CONTENT_ESCAPE);
                        return StringUtils.removeEnd(tempContent,EXCEL_CONTENT_ESCAPE);
                    }else{
                        return content;
                    }
                })
                .toList();
    }

    private String toExcelContent(List<ClipboardCell> cells){
        List<Integer> rows = cells.stream()
                .map(ClipboardCell::getRow)
                .distinct()
                .sorted()
                .toList();
        return rows.stream()
                .map(row->{
                    var cellContents = getExcelRowContent(cells,row);
                    var emptyColumn = cellContents.stream().filter(StringUtils::isBlank).count();
                    return emptyColumn == cellContents.size() ? StringUtils.EMPTY : String.join(EXCEL_COLUMN_SPLIT,cellContents);
                })
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.joining(EXCEL_ROW_SPLIT));
    }

    private List<String> getExcelRowContent(List<ClipboardCell> cells,Integer row){
        return cells.stream()
                .filter(c->c.getRow()==row && Objects.nonNull(c.getValue()))
                .sorted(Comparator.comparingInt(ClipboardCell::getColumn))
                .map(cell->{
                    var content = cell.getValue().toString();
                    if(StringUtils.containsAny(content,EXCEL_COLUMN_SPLIT,EXCEL_ROW_SPLIT)){
                        content = StringUtils.replace(content,EXCEL_CONTENT_ESCAPE,EXCEL_CANCEL_ESCAPE);
                        return EXCEL_CONTENT_ESCAPE.concat(content).concat(EXCEL_CONTENT_ESCAPE);
                    }else{
                        return content;
                    }
                })
                .toList();
    }

    protected ContextMenu generateContextMenu(boolean hasDel){
        final ContextMenu contextMenu = new ContextMenu();
        final MenuItem copyItem = new MenuItem("复制 Ctrl+C", IconUtils.getGlyph("cell-copy-icon","context-menu-icon"));
        copyItem.setOnAction(e -> copyClipboard());
        final MenuItem pasteItem = new MenuItem("粘贴 Ctrl+V",IconUtils.getGlyph("cell-paste-icon","context-menu-icon"));
        pasteItem.setOnAction(e -> pasteClipboard());
        final MenuItem clearItem = new MenuItem("清空 Delete",IconUtils.getGlyph("cell-clear-icon","context-menu-icon"));
        clearItem.setOnAction(e -> clearSelectedContent());
        if(hasDel){
            final MenuItem delItem = new MenuItem("删除 Ctrl+Delete",IconUtils.getGlyph("del-icon","context-menu-icon"));
            delItem.setOnAction(e -> deleteData());
            contextMenu.getItems().addAll(copyItem, pasteItem, clearItem,delItem);
        }else{
            contextMenu.getItems().addAll(copyItem, pasteItem, clearItem);
        }
        return contextMenu;
    }

    protected void setCornerTag(SpreadsheetCell cell){
        cell.activateCorner(SpreadsheetCell.CornerPosition.TOP_LEFT);
    }

    protected Integer toDataIndex(@NonNull Integer sheetRowIndex){
        return sheetRowIndex;
    }

    protected Integer toSheetRowIndex(@NonNull Integer dataIndex){
        return dataIndex;
    }

    protected Set<Integer> selectedSheetRowIndex(){
        return this.getSelectionModel()
                .getSelectedCells()
                .stream()
                .map(tp->getModelRow(tp.getRow()))
                .collect(Collectors.toSet());
    }

    protected void clearSelectedContent(){
        var selectedCells = this.getSelectionModel().getSelectedCells();
        if (!isEditable() || selectedCells.isEmpty()) {
            return;
        }
        for (final TablePosition<?, ?> p : selectedCells) {
            SpreadsheetCell cell = getGrid().getRows().get(getModelRow(p.getRow())).get(getModelColumn(p.getColumn()));
            if (cell.isEditable()) {
                cell.setItem("");
            }
        }
    }

    protected SpreadsheetCell getTextFieldCell(int row,int column){
        return getTextFieldCell(row,column,true);
    }

    protected SpreadsheetCell getTextFieldCell(int row,int column,boolean editable){
        return getTextFieldCell(row,column,"",editable);
    }

    protected void checkCellChanged(ObservableValue<? extends String> observableValue, String oldValue, String newValue){
        if(editModel.get()){
            change.set(!StringUtils.equals(oldValue,newValue));
        }
    }

    protected SpreadsheetCell getTextFieldCell(int row,int column,String value,boolean editable){
        var cell = SpreadsheetCellType.STRING
                .createCell(row,column,1,1,value);
        cell.setEditable(editable);
        cell.getStyleClass().add("data-cell");
        cell.textProperty().addListener(this::checkCellChanged);
        if(!editable){
            cell.getStyleClass().add("data-disable-cell");
        }
        return cell;
    }

    protected void setTextFieldCell(SpreadsheetCell cell,String value){
        if(cell.isEditable()){
            cell.setItem(value);
        }else{
            cell.setEditable(true);
            cell.setItem(value);
            cell.setEditable(false);
        }
    }

    protected SpreadsheetCell getDatePickerCell(int row, int column, LocalDate value){
        var cell = SpreadsheetCellType.DATE
                .createCell(row,column,1,1,value);
        cell.getStyleClass().add("data-cell");
        cell.textProperty().addListener(this::checkCellChanged);
        return cell;
    }

    protected SpreadsheetCell getTextAreaCell(int row,int column){
        return getTextAreaCell(row,column,"data-cell");
    }

    protected SpreadsheetCell getTextAreaCell(int row,int column,String styleClass){
        return getTextAreaCell(row,column,"",true,styleClass);
    }

    protected SpreadsheetCell getTextAreaCell(int row,int column,String value,String styleClass,boolean editable){
        return getTextAreaCell(row,column,value,editable,styleClass);
    }

    protected SpreadsheetCell getTextAreaCell(int row,int column,String value,boolean editable){
        return getTextAreaCell(row,column,value,editable,"data-cell");
    }

    protected SpreadsheetCell getTextAreaCell(int row,int column,String value,boolean editable,String styleClass){
        var cell = PesSheetCellType.TEXT_AREA
                .createCell(row,column,1,1,value);
        cell.setEditable(editable);
        cell.setWrapText(true);
        cell.getStyleClass().add(styleClass);
        cell.textProperty().addListener(this::checkCellChanged);
        if(!editable){
            cell.getStyleClass().add("data-disable-cell");
        }
        return cell;
    }

    protected <ItemType> SpreadsheetCell getComboBoxCell(int row,int column,ObservableList<ItemType> items){
        return getComboBoxCell(row,column,items,"");
    }

    protected <ItemType> SpreadsheetCell getComboBoxCell(int row,int column,ObservableList<ItemType> items,String value){
        var cell = PesSheetCellType.LIST(items)
                .createCell(row,column,1,1,value);
        cell.setEditable(true);
        cell.getStyleClass().add("data-cell");
        cell.textProperty().addListener(this::checkCellChanged);
        return cell;
    }
    protected <ItemType> SpreadsheetCell getCheckComboBoxCell(int row,int column,ObservableList<ItemType> items,String splitChar){
        var cell = PesSheetCellType.CHECK_LIST(items,splitChar)
                .createCell(row,column,1,1,"");
        cell.setEditable(true);
        cell.setWrapText(true);
        cell.getStyleClass().add("data-cell");
        cell.textProperty().addListener(this::checkCellChanged);
        return cell;
    }

    protected SpreadsheetCell getOptionCell(int row,int column){
        var cell = SpreadsheetCellType.STRING
                .createCell(row,column,1,1,"");
        cell.setEditable(false);
        cell.getStyleClass().add("data-option-cell");
        return cell;
    }

    /**
     * 删除数据
     */
    protected abstract void deleteData();

}